package calao.storage;

import java.util.HashMap;
import java.util.Vector;

/** A storage table.
 */
public class Table 
extends SchemaElement
{

	static HashMap<String,Table> MAP = new HashMap<String,Table>();

	private final Vector<Field> fields = new Vector<Field>();
	private final Vector<Index> indexes = new Vector<Index>();

	Index primaryKeyIndex;

	/** Creates a new table.
	 * 
	 * @param name the name of the new table.
	 * @param fields
	 */
	public Table(String name)
	{
		super(name);
	}

	/** Gets the name of this table.
	 */
	public String getName()
	{
		return name;
	}

	@Override
	protected void register() 
	throws InvalidSchemaException
	{
		Table current = MAP.get(name);
		if (current!=this) {
			if (current!=null) {
				throw new InvalidSchemaException(name+": table name already used");
			}
			if (primaryKeyIndex==null) {
				throw new InvalidSchemaException(name+": missing primary key index");
			}
			MAP.put(name, this);
		}
	}

	/** Creates a primary key index.
	 * As for other key indexes, primary key indexes do no accept multi-valued fields.
	 * @param fields the index fields, in order. 
	 */
	public Index createPrimaryKeyIndex(Field... fields)
	{
		ensureNotRegistered();
		if (primaryKeyIndex!=null) {
			throw new RuntimeException("Key index already exists");
		}
		for (Field field : fields) {
			if (field.isMultiValued()) {
				throw new IllegalArgumentException(
						"Cannot create primary key index on multivalued field "
						+field.getName());
			}
		}
		primaryKeyIndex = new Index(this, fields, true);
		indexes.add(primaryKeyIndex);
		return primaryKeyIndex;
	}

	/** Creates a key index.
	 * Key indexes do no accept multi-valued fields.
	 * @param fields the index fields, in order. 
	 */
	public Index createKeyIndex(Field... fields)
	{
		ensureNotRegistered();
		for (int i=0; i<fields.length; i++) {
			if (fields[i].isMultiValued()) throw new IllegalArgumentException();
		}
		Index index = new Index(this, fields, true);
		indexes.add(index);
		return index;
	}

	/** Creates an index.
	 * The first field of an index can be multi-valued.
	 * @param fields the index fields, in order. 
	 */
	public Index createIndex(Field... fields)
	{
		ensureNotRegistered();
		for (int i=1; i<fields.length; i++) {
			if (fields[i].isMultiValued()) throw new IllegalArgumentException();
		}
		Index index = new Index(this, fields, false);
		indexes.add(index);
		return index;
	}

	/** Gets a registered table.
	 * 
	 * @param name the requested table name.
	 * @return The table registered with the specified name, or null if there is
	 * no such table. 
	 */
	public static Table getTable(String name)
	{
		return MAP.get(name);
	}

	/** Creates a single-valued primitive field.
	 * 
	 * @param name the name of the new field.
	 * @param type the type of the new field. 
	 */
	public <T> Field<T> createField(String name, FieldType<T> type)
	{
		return createField(name, type, false);
	}

	/** Creates a primitive field.
	 * 
	 * @param name the name of the new field.
	 * @param type the type of the new field. 
	 * @param multiValued whether the new field accepts multiple values.
	 */
	public <T> Field<T> createField(String name, FieldType<T> type, boolean multiValued)
	{
		Field<T> field = new Field<T>(this, name, type, multiValued);
		add(field);
		return field;
	}

	/** Creates a single-valued object field.
	 * @param name the name of the new field.
	 * @param type the base type for all values that can be assigned to this field.
	 */
	public <T> Field<T> createField(String name, Class<T> type)
	{
		return createField(name, type, false);
	}

	/** Creates an object field.
	 * @param name the name of the new field.
	 * @param type the base type for all values that can be assigned to this field.
	 * @param multiValued whether the new field accepts multiple values.
	 */
	public <T> Field<T> createField(String name, Class<T> type, boolean multiValued)
	{
		FieldType<T> ftype = new FieldType<T>(type,	FieldType.Enum.OBJECT);
		Field<T> field = new Field<T>(this, name, ftype, multiValued);
		add(field);
		return field;
	}

	/** Creates a single-valued object field.
	 * @param name the name of the new field.
	 * @param valueSample a sample value used to set the base type for all values that can
	 * be assigned to this field.
	 */
	public <T> Field<T> createField(String name, T valueSample)
	{
		return createField(name, valueSample, false);
	}

	/** Creates an object field.
	 * @param name the name of the new field.
	 * @param valueSample a sample value used to set the base type for all values that can
	 * be assigned to this field.
	 * @param multiValued whether the new field accepts multiple values.
	 */
	@SuppressWarnings("unchecked")
	public <T> Field<T> createField(String name, T valueSample, boolean multiValued)
	{
		FieldType<T> type = new FieldType<T>((Class<T>)valueSample.getClass(), 
				FieldType.Enum.OBJECT);
		Field<T> field = new Field<T>(this, name, type, multiValued);
		add(field);
		return field;
	}

	private void add(Field field) 
	{
		ensureNotRegistered();
		for (Field efield : fields) if (field.name.equals(efield.name)) {
			throw new IllegalArgumentException("Field "+field.name+" already exists");
		}
		fields.add(field);
	}

	/** Gets fields in this table.
	 */
	public Iterable<Field> getFieldList()
	{
		return fields;
	}

	/** Gets the number of fields in this table.
	 */
	public int getFieldCount()
	{
		return fields.size();
	}

	/** Gets indexes associated with this table.
	 */
	public Iterable<Index> getIndexList()
	{
		return indexes;
	}

	/** Gets the table primary key index.
	 */
	public Index getPrimaryKeyIndex()
	{
		return primaryKeyIndex;
	}

	/** Gets a field by position.
	 */
	public Field getField(int position) 
	{
		return fields.get(position);
	}

	/** Gets a field.
	 * 
	 * @param name the name of the requested field.
	 */
	public Field getField(String name)
	{
		for (Field field : fields) {
			if (field.getName().equals(name)) return field;
		}
		return null;
	}

	/** Gets all tables.
	 */
	public static Iterable<Table> getRegisteredTables()
	{
		return MAP.values();
	}

	/** Gets a count of indexes.
	 */
	public int getIndexCount()
	{
		return indexes.size();
	}

	/** Creates a new search on this table.
	 */
	public Search newSearch()
	{
		return new Search(this);
	}

}
